/*
 * charts for WeChat small app v1.0
 *
 * https://github.com/xiaolin3303/wx-charts
 * 2016-11-28
 *
 * Designed and built with all the love of Web
 */

'use strict'

var config = {
  yAxisWidth: 15,
  yAxisSplit: 5,
  xAxisHeight: 15,
  xAxisLineHeight: 15,
  legendHeight: 15,
  yAxisTitleWidth: 15,
  padding: 12,
  columePadding: 3,
  fontSize: 12,
  dataPointShape: ['diamond', 'circle', 'triangle', 'rect'],
  colors: ['#7cb5ec', '#f7a35c', '#434348', '#90ed7d', '#f15c80', '#8085e9'],
  pieChartLinePadding: 25,
  pieChartTextPadding: 15,
  xAxisTextPadding: 3,
  titleColor: '#333333',
  titleFontSize: 20,
  subtitleColor: '#999999',
  subtitleFontSize: 15,
  toolTipPadding: 3,
  toolTipBackground: '#000000',
  toolTipOpacity: 0.7,
  toolTipLineHeight: 14,
  radarGridCount: 3,
  radarLabelTextMargin: 15
}

// Object.assign polyfill
// https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
function assign (target, varArgs) {
  if (target == null) {
    // TypeError if undefined or null
    throw new TypeError('Cannot convert undefined or null to object')
  }

  var to = Object(target)

  for (var index = 1; index < arguments.length; index++) {
    var nextSource = arguments[index]

    if (nextSource != null) {
      // Skip over if undefined or null
      for (var nextKey in nextSource) {
        // Avoid bugs when hasOwnProperty is shadowed
        if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
          to[nextKey] = nextSource[nextKey]
        }
      }
    }
  }
  return to
}

var util = {
  toFixed: function toFixed (num, limit) {
    limit = limit || 2
    if (this.isFloat(num)) {
      num = num.toFixed(limit)
    }
    return num
  },
  isFloat: function isFloat (num) {
    return num % 1 !== 0
  },
  approximatelyEqual: function approximatelyEqual (num1, num2) {
    return Math.abs(num1 - num2) < 1e-10
  },
  isSameSign: function isSameSign (num1, num2) {
    return Math.abs(num1) === num1 && Math.abs(num2) === num2 || Math.abs(num1) !== num1 && Math.abs(num2) !== num2
  },
  isSameXCoordinateArea: function isSameXCoordinateArea (p1, p2) {
    return this.isSameSign(p1.x, p2.x)
  },
  isCollision: function isCollision (obj1, obj2) {
    obj1.end = {}
    obj1.end.x = obj1.start.x + obj1.width
    obj1.end.y = obj1.start.y - obj1.height
    obj2.end = {}
    obj2.end.x = obj2.start.x + obj2.width
    obj2.end.y = obj2.start.y - obj2.height
    var flag = obj2.start.x > obj1.end.x || obj2.end.x < obj1.start.x || obj2.end.y > obj1.start.y || obj2.start.y < obj1.end.y

    return !flag
  }
}

function findRange (num, type, limit) {
  if (isNaN(num)) {
    throw new Error('[wxCharts] unvalid series data!')
  }
  limit = limit || 10
  type = type ? type : 'upper'
  var multiple = 1
  while (limit < 1) {
    limit *= 10
    multiple *= 10
  }
  if (type === 'upper') {
    num = Math.ceil(num * multiple)
  } else {
    num = Math.floor(num * multiple)
  }
  while (num % limit !== 0) {
    if (type === 'upper') {
      num++
    } else {
      num--
    }
  }

  return num / multiple
}

function calValidDistance (distance, chartData, config, opts) {

  var dataChartAreaWidth = opts.width - config.padding - chartData.xAxisPoints[0]
  var dataChartWidth = chartData.eachSpacing * opts.categories.length
  var validDistance = distance
  if (distance >= 0) {
    validDistance = 0
  } else if (Math.abs(distance) >= dataChartWidth - dataChartAreaWidth) {
    validDistance = dataChartAreaWidth - dataChartWidth
  }
  return validDistance
}

function isInAngleRange (angle, startAngle, endAngle) {
  function adjust (angle) {
    while (angle < 0) {
      angle += 2 * Math.PI
    }
    while (angle > 2 * Math.PI) {
      angle -= 2 * Math.PI
    }

    return angle
  }

  angle = adjust(angle)
  startAngle = adjust(startAngle)
  endAngle = adjust(endAngle)
  if (startAngle > endAngle) {
    endAngle += 2 * Math.PI
    if (angle < startAngle) {
      angle += 2 * Math.PI
    }
  }

  return angle >= startAngle && angle <= endAngle
}

function calRotateTranslate (x, y, h) {
  var xv = x
  var yv = h - y

  var transX = xv + (h - yv - xv) / Math.sqrt(2)
  transX *= -1

  var transY = (h - yv) * (Math.sqrt(2) - 1) - (h - yv - xv) / Math.sqrt(2)

  return {
    transX: transX,
    transY: transY
  }
}

function createCurveControlPoints (points, i) {

  function isNotMiddlePoint (points, i) {
    if (points[i - 1] && points[i + 1]) {
      return points[i].y >= Math.max(points[i - 1].y, points[i + 1].y) || points[i].y <= Math.min(points[i - 1].y, points[i + 1].y)
    } else {
      return false
    }
  }

  var a = 0.2
  var b = 0.2
  var pAx = null
  var pAy = null
  var pBx = null
  var pBy = null
  if (i < 1) {
    pAx = points[0].x + (points[1].x - points[0].x) * a
    pAy = points[0].y + (points[1].y - points[0].y) * a
  } else {
    pAx = points[i].x + (points[i + 1].x - points[i - 1].x) * a
    pAy = points[i].y + (points[i + 1].y - points[i - 1].y) * a
  }

  if (i > points.length - 3) {
    var last = points.length - 1
    pBx = points[last].x - (points[last].x - points[last - 1].x) * b
    pBy = points[last].y - (points[last].y - points[last - 1].y) * b
  } else {
    pBx = points[i + 1].x - (points[i + 2].x - points[i].x) * b
    pBy = points[i + 1].y - (points[i + 2].y - points[i].y) * b
  }

  // fix issue https://github.com/xiaolin3303/wx-charts/issues/79
  if (isNotMiddlePoint(points, i + 1)) {
    pBy = points[i + 1].y
  }
  if (isNotMiddlePoint(points, i)) {
    pAy = points[i].y
  }

  return {
    ctrA: {x: pAx, y: pAy},
    ctrB: {x: pBx, y: pBy}
  }
}

function convertCoordinateOrigin (x, y, center) {
  return {
    x: center.x + x,
    y: center.y - y
  }
}

function avoidCollision (obj, target) {
  if (target) {
    // is collision test
    while (util.isCollision(obj, target)) {
      if (obj.start.x > 0) {
        obj.start.y--
      } else if (obj.start.x < 0) {
        obj.start.y++
      } else {
        if (obj.start.y > 0) {
          obj.start.y++
        } else {
          obj.start.y--
        }
      }
    }
  }
  return obj
}

function fillSeriesColor (series, config) {
  var index = 0
  return series.map(function (item) {
    if (!item.color) {
      item.color = config.colors[index]
      index = (index + 1) % config.colors.length
    }
    return item
  })
}

function getDataRange (minData, maxData) {
  var limit = 0
  var range = maxData - minData
  if (range >= 10000) {
    limit = 1000
  } else if (range >= 1000) {
    limit = 100
  } else if (range >= 100) {
    limit = 10
  } else if (range >= 10) {
    limit = 5
  } else if (range >= 1) {
    limit = 1
  } else if (range >= 0.1) {
    limit = 0.1
  } else {
    limit = 0.01
  }
  return {
    minRange: findRange(minData, 'lower', limit),
    maxRange: findRange(maxData, 'upper', limit)
  }
}

function measureText (text) {
  var fontSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : config.fontSize

  // wx canvas 未实现measureText方法, 此处自行实现
  text = String(text)
  text = text.split('')
  var width = 0
  text.forEach(function (item) {
    if (/[a-zA-Z]/.test(item)) {
      width += 7
    } else if (/[0-9]/.test(item)) {
      width += 5.5
    } else if (/\./.test(item)) {
      width += 2.7
    } else if (/-/.test(item)) {
      width += 3.25
    } else if (/[\u4e00-\u9fa5]/.test(item)) {
      width += 10
    } else if (/\(|\)/.test(item)) {
      width += 3.73
    } else if (/\s/.test(item)) {
      width += 2.5
    } else if (/%/.test(item)) {
      width += 8
    } else {
      width += 10
    }
  })
  return width * fontSize / 10
}

function calcTextArr (str, limitWidth) {
  var lineWidth = 0
  var strArr = str.split('')

  for (var i = 0; i < strArr.length; i++) {
    if (strArr[i] === '\n') {
      lineWidth = 0
      continue
    }
    lineWidth += measureText(strArr[i])
    if (lineWidth > limitWidth) {
      lineWidth = 0
      strArr.splice(i, 0, '\n')
    }
  }

  return strArr.join('')
}

function dataCombine (series) {
  return series.reduce(function (a, b) {
    return (a.data ? a.data : a).concat(b.data)
  }, [])
}

function getSeriesDataItem (series, index) {
  var data = []
  series.forEach(function (item) {
    if (item.data[index] !== null && typeof item.data[index] !== 'undefined') {
      var seriesItem = {}
      seriesItem.color = item.color
      seriesItem.name = item.name
      seriesItem.data = item.format ? item.format(item.data[index]) : item.data[index]
      data.push(seriesItem)
    }
  })

  return data
}

function getMaxTextListLength (list) {
  var lengthList = list.map(function (item) {
    return measureText(item)
  })
  return Math.max.apply(null, lengthList)
}

function getRadarCoordinateSeries (length) {
  var eachAngle = 2 * Math.PI / length
  var CoordinateSeries = []
  for (var i = 0; i < length; i++) {
    CoordinateSeries.push(eachAngle * i)
  }

  return CoordinateSeries.map(function (item) {
    return -1 * item + Math.PI / 2
  })
}

function getToolTipData (seriesData, calPoints, index, categories) {
  var option = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}

  var textList = seriesData.map(function (item) {
    return {
      text: option.format ? option.format(item, categories[index]) : item.name + ': ' + item.data,
      color: item.color
    }
  })
  var validCalPoints = []
  var offset = {
    x: 0,
    y: 0
  }
  calPoints.forEach(function (points) {
    if (typeof points[index] !== 'undefined' && points[index] !== null) {
      validCalPoints.push(points[index])
    }
  })
  validCalPoints.forEach(function (item) {
    offset.x = Math.round(item.x)
    offset.y += item.y
  })

  offset.y /= validCalPoints.length
  return {textList: textList, offset: offset}
}

function findCurrentIndex (currentPoints, xAxisPoints, opts, config) {
  var offset = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0

  var currentIndex = -1
  if (isInExactChartArea(currentPoints, opts, config)) {
    xAxisPoints.forEach(function (item, index) {
      if (currentPoints.x + offset > item) {
        currentIndex = index
      }
    })
  }

  return currentIndex
}

function isInExactChartArea (currentPoints, opts, config) {
  return currentPoints.x < opts.width - config.padding && currentPoints.x > config.padding + config.yAxisWidth + config.yAxisTitleWidth && currentPoints.y > config.padding && currentPoints.y < opts.height - config.legendHeight - config.xAxisHeight - config.padding
}

function findRadarChartCurrentIndex (currentPoints, radarData, count) {
  var eachAngleArea = 2 * Math.PI / count
  var currentIndex = -1
  if (isInExactPieChartArea(currentPoints, radarData.center, radarData.radius)) {
    var fixAngle = function fixAngle (angle) {
      if (angle < 0) {
        angle += 2 * Math.PI
      }
      if (angle > 2 * Math.PI) {
        angle -= 2 * Math.PI
      }
      return angle
    }

    var angle = Math.atan2(radarData.center.y - currentPoints.y, currentPoints.x - radarData.center.x)
    angle = -1 * angle
    if (angle < 0) {
      angle += 2 * Math.PI
    }

    var angleList = radarData.angleList.map(function (item) {
      item = fixAngle(-1 * item)

      return item
    })

    angleList.forEach(function (item, index) {
      var rangeStart = fixAngle(item - eachAngleArea / 2)
      var rangeEnd = fixAngle(item + eachAngleArea / 2)
      if (rangeEnd < rangeStart) {
        rangeEnd += 2 * Math.PI
      }
      if (angle >= rangeStart && angle <= rangeEnd || angle + 2 * Math.PI >= rangeStart && angle + 2 * Math.PI <= rangeEnd) {
        currentIndex = index
      }
    })
  }

  return currentIndex
}

function findPieChartCurrentIndex (currentPoints, pieData) {
  var currentIndex = -1
  if (isInExactPieChartArea(currentPoints, pieData.center, pieData.radius)) {
    var angle = Math.atan2(pieData.center.y - currentPoints.y, currentPoints.x - pieData.center.x)
    angle = -angle
    for (var i = 0, len = pieData.series.length; i < len; i++) {
      var item = pieData.series[i]
      if (isInAngleRange(angle, item._start_, item._start_ + item._proportion_ * 2 * Math.PI)) {
        currentIndex = i
        break
      }
    }
  }

  return currentIndex
}

function isInExactPieChartArea (currentPoints, center, radius) {
  return Math.pow(currentPoints.x - center.x, 2) + Math.pow(currentPoints.y - center.y, 2) <= Math.pow(radius, 2)
}

function splitPoints (points) {
  var newPoints = []
  var items = []
  points.forEach(function (item, index) {
    if (item !== null) {
      items.push(item)
    } else {
      if (items.length) {
        newPoints.push(items)
      }
      items = []
    }
  })
  if (items.length) {
    newPoints.push(items)
  }

  return newPoints
}

function calLegendData (series, opts, config) {
  if (opts.legend === false) {
    return {
      legendList: [],
      legendHeight: 0
    }
  }
  var padding = 5
  var marginTop = 8
  var shapeWidth = 15
  var legendList = []
  var widthCount = 0
  var currentRow = []
  series.forEach(function (item) {
    var itemWidth = 3 * padding + shapeWidth + measureText(item.name || 'undefined')
    if (widthCount + itemWidth > opts.width) {
      legendList.push(currentRow)
      widthCount = itemWidth
      currentRow = [item]
    } else {
      widthCount += itemWidth
      currentRow.push(item)
    }
  })
  if (currentRow.length) {
    legendList.push(currentRow)
  }

  return {
    legendList: legendList,
    legendHeight: legendList.length * (config.fontSize + marginTop) + padding
  }
}

function calCategoriesData (categories, opts, config) {
  var result = {
    angle: 0,
    xAxisHeight: config.xAxisHeight
  }

  var _getXAxisPoints = getXAxisPoints(categories, opts, config),
    eachSpacing = _getXAxisPoints.eachSpacing

  // get max length of categories text

  var categoriesTextLenth = categories.map(function (item) {
    return measureText(item)
  })

  var maxTextLength = Math.max.apply(this, categoriesTextLenth)

  if (maxTextLength + 2 * config.xAxisTextPadding > eachSpacing) {
    result.angle = 45 * Math.PI / 180
    result.xAxisHeight = 2 * config.xAxisTextPadding + maxTextLength * Math.sin(result.angle)
  }

  return result
}

function getRadarDataPoints (angleList, center, radius, series, opts) {
  var process = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 1

  var radarOption = opts.extra.radar || {}
  radarOption.max = radarOption.max || 0
  var maxData = Math.max(radarOption.max, Math.max.apply(null, dataCombine(series)))

  var data = []
  series.forEach(function (each) {
    var listItem = {}
    listItem.color = each.color
    listItem.data = []
    each.data.forEach(function (item, index) {
      var tmp = {}
      tmp.angle = angleList[index]

      tmp.proportion = item / maxData
      tmp.position = convertCoordinateOrigin(radius * tmp.proportion * process * Math.cos(tmp.angle), radius * tmp.proportion * process * Math.sin(tmp.angle), center)
      listItem.data.push(tmp)
    })

    data.push(listItem)
  })

  return data
}

function getPieDataPoints (series) {
  var process = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1

  var count = 0
  var _start_ = 0
  series.forEach(function (item) {
    item.data = item.data === null ? 0 : item.data
    count += item.data
  })
  series.forEach(function (item) {
    item.data = item.data === null ? 0 : item.data
    item._proportion_ = item.data / count * process
  })
  series.forEach(function (item) {
    item._start_ = _start_
    _start_ += 2 * item._proportion_ * Math.PI
  })

  return series
}

function getPieTextMaxLength (series) {
  series = getPieDataPoints(series)
  var maxLength = 0
  series.forEach(function (item) {
    var text = item.format ? item.format(+item._proportion_.toFixed(2)) : util.toFixed(item._proportion_ * 100) + '%'
    maxLength = Math.max(maxLength, measureText(text))
  })

  return maxLength
}

function fixColumeData (points, eachSpacing, columnLen, index, config, opts) {
  return points.map(function (item) {
    if (item === null) {
      return null
    }
    item.width = (eachSpacing - 2 * config.columePadding) / columnLen

    if (opts.extra.column && opts.extra.column.width && +opts.extra.column.width > 0) {
      // customer column width
      item.width = Math.min(item.width, +opts.extra.column.width)
    } else {
      // default width should less tran 25px
      // don't ask me why, I don't know
      item.width = Math.min(item.width, 25)
    }
    item.x += (index + 0.5 - columnLen / 2) * item.width

    return item
  })
}

function getXAxisPoints (categories, opts, config) {
  var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
  var spacingValid = opts.width - 2 * config.padding - yAxisTotalWidth
  var dataCount = opts.enableScroll ? Math.min(5, categories.length) : categories.length
  var eachSpacing = spacingValid / dataCount

  var xAxisPoints = []
  var startX = config.padding + yAxisTotalWidth
  var endX = opts.width - config.padding
  categories.forEach(function (item, index) {
    xAxisPoints.push(startX + index * eachSpacing)
  })
  if (opts.enableScroll === true) {
    xAxisPoints.push(startX + categories.length * eachSpacing)
  } else {
    xAxisPoints.push(endX)
  }

  return {xAxisPoints: xAxisPoints, startX: startX, endX: endX, eachSpacing: eachSpacing}
}

function getDataPoints (data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config) {
  var process = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : 1

  var points = []
  var validHeight = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
  data.forEach(function (item, index) {
    if (item === null) {
      points.push(null)
    } else {
      var point = {}
      point.x = xAxisPoints[index] + Math.round(eachSpacing / 2)
      var height = validHeight * (item - minRange) / (maxRange - minRange)
      height *= process
      point.y = opts.height - config.xAxisHeight - config.legendHeight - Math.round(height) - config.padding
      points.push(point)
    }
  })

  return points
}

function getYAxisTextList (series, opts, config) {
  var data = dataCombine(series)
  // remove null from data
  data = data.filter(function (item) {
    return item !== null
  })
  var minData = Math.min.apply(this, data)
  var maxData = Math.max.apply(this, data)
  if (typeof opts.yAxis.min === 'number') {
    minData = Math.min(opts.yAxis.min, minData)
  }
  if (typeof opts.yAxis.max === 'number') {
    maxData = Math.max(opts.yAxis.max, maxData)
  }

  // fix issue https://github.com/xiaolin3303/wx-charts/issues/9
  if (minData === maxData) {
    var rangeSpan = maxData || 1
    minData -= rangeSpan
    maxData += rangeSpan
  }

  var dataRange = getDataRange(minData, maxData)
  var minRange = dataRange.minRange
  var maxRange = dataRange.maxRange

  var range = []
  var eachRange = (maxRange - minRange) / config.yAxisSplit

  for (var i = 0; i <= config.yAxisSplit; i++) {
    range.push(minRange + eachRange * i)
  }
  return range.reverse()
}

function calYAxisData (series, opts, config) {

  var ranges = getYAxisTextList(series, opts, config)
  var yAxisWidth = config.yAxisWidth
  var rangesFormat = ranges.map(function (item) {
    item = util.toFixed(item, 2)
    item = opts.yAxis.format ? opts.yAxis.format(Number(item)) : item
    yAxisWidth = Math.max(yAxisWidth, measureText(item) + 5)
    return item
  })
  if (opts.yAxis.disabled === true) {
    yAxisWidth = 0
  }

  return {rangesFormat: rangesFormat, ranges: ranges, yAxisWidth: yAxisWidth}
}

function drawPointShape (points, color, shape, context) {
  context.beginPath()
  context.setStrokeStyle('#ffffff')
  context.setLineWidth(1)
  context.setFillStyle(color)

  if (shape === 'diamond') {
    points.forEach(function (item, index) {
      if (item !== null) {
        context.moveTo(item.x, item.y - 4.5)
        context.lineTo(item.x - 4.5, item.y)
        context.lineTo(item.x, item.y + 4.5)
        context.lineTo(item.x + 4.5, item.y)
        context.lineTo(item.x, item.y - 4.5)
      }
    })
  } else if (shape === 'circle') {
    points.forEach(function (item, index) {
      if (item !== null) {
        context.moveTo(item.x + 3.5, item.y)
        context.arc(item.x, item.y, 4, 0, 2 * Math.PI, false)
      }
    })
  } else if (shape === 'rect') {
    points.forEach(function (item, index) {
      if (item !== null) {
        context.moveTo(item.x - 3.5, item.y - 3.5)
        context.rect(item.x - 3.5, item.y - 3.5, 7, 7)
      }
    })
  } else if (shape === 'triangle') {
    points.forEach(function (item, index) {
      if (item !== null) {
        context.moveTo(item.x, item.y - 4.5)
        context.lineTo(item.x - 4.5, item.y + 4.5)
        context.lineTo(item.x + 4.5, item.y + 4.5)
        context.lineTo(item.x, item.y - 4.5)
      }
    })
  }
  context.closePath()
  context.fill()
  context.stroke()
}

function drawRingTitle (opts, config, context) {
  var titlefontSize = opts.title.fontSize || config.titleFontSize
  var subtitlefontSize = opts.subtitle.fontSize || config.subtitleFontSize
  var title = opts.title.name || ''
  var subtitle = opts.subtitle.name || ''
  var titleFontColor = opts.title.color || config.titleColor
  var subtitleFontColor = opts.subtitle.color || config.subtitleColor
  var titleHeight = title ? titlefontSize : 0
  var subtitleHeight = subtitle ? subtitlefontSize : 0
  var margin = 5
  if (subtitle) {
    var textWidth = measureText(subtitle, subtitlefontSize)
    var startX = (opts.width - textWidth) / 2 + (opts.subtitle.offsetX || 0)
    var startY = (opts.height - config.legendHeight + subtitlefontSize) / 2
    if (title) {
      startY -= (titleHeight + margin) / 2
    }
    context.beginPath()
    context.setFontSize(subtitlefontSize)
    context.setFillStyle(subtitleFontColor)
    context.fillText(subtitle, startX, startY)
    context.stroke()
    context.closePath()
  }
  if (title) {
    var _textWidth = measureText(title, titlefontSize)
    var _startX = (opts.width - _textWidth) / 2 + (opts.title.offsetX || 0)
    var _startY = (opts.height - config.legendHeight + titlefontSize) / 2
    if (subtitle) {
      _startY += (subtitleHeight + margin) / 2
    }
    context.beginPath()
    context.setFontSize(titlefontSize)
    context.setFillStyle(titleFontColor)
    context.fillText(title, _startX, _startY)
    context.stroke()
    context.closePath()
  }
}

function drawPointText (points, series, config, context) {
  // 绘制数据文案
  var data = series.data

  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle('#666666')
  points.forEach(function (item, index) {
    if (item !== null) {
      var formatVal = series.format ? series.format(data[index]) : data[index]
      context.fillText(formatVal, item.x - measureText(formatVal) / 2, item.y - 2)
    }
  })
  context.closePath()
  context.stroke()
}

function drawRadarLabel (angleList, radius, centerPosition, opts, config, context) {
  var radarOption = opts.extra.radar || {}
  radius += config.radarLabelTextMargin
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle(radarOption.labelColor || '#666666')
  angleList.forEach(function (angle, index) {
    var pos = {
      x: radius * Math.cos(angle),
      y: radius * Math.sin(angle)
    }
    var posRelativeCanvas = convertCoordinateOrigin(pos.x, pos.y, centerPosition)
    var startX = posRelativeCanvas.x
    var startY = posRelativeCanvas.y
    if (util.approximatelyEqual(pos.x, 0)) {
      startX -= measureText(opts.categories[index] || '') / 2
    } else if (pos.x < 0) {
      startX -= measureText(opts.categories[index] || '')
    }
    context.fillText(opts.categories[index] || '', startX, startY + config.fontSize / 2)
  })
  context.stroke()
  context.closePath()
}

function drawPieText (series, opts, config, context, radius, center) {
  var lineRadius = radius + config.pieChartLinePadding
  var textObjectCollection = []
  var lastTextObject = null

  var seriesConvert = series.map(function (item) {
    var arc = 2 * Math.PI - (item._start_ + 2 * Math.PI * item._proportion_ / 2)
    var text = item.format ? item.format(+item._proportion_.toFixed(2)) : util.toFixed(item._proportion_ * 100) + '%'
    var color = item.color
    return {arc: arc, text: text, color: color}
  })
  seriesConvert.forEach(function (item) {
    // line end
    var orginX1 = Math.cos(item.arc) * lineRadius
    var orginY1 = Math.sin(item.arc) * lineRadius

    // line start
    var orginX2 = Math.cos(item.arc) * radius
    var orginY2 = Math.sin(item.arc) * radius

    // text start
    var orginX3 = orginX1 >= 0 ? orginX1 + config.pieChartTextPadding : orginX1 - config.pieChartTextPadding
    var orginY3 = orginY1

    var textWidth = measureText(item.text)
    var startY = orginY3

    if (lastTextObject && util.isSameXCoordinateArea(lastTextObject.start, {x: orginX3})) {
      if (orginX3 > 0) {
        startY = Math.min(orginY3, lastTextObject.start.y)
      } else if (orginX1 < 0) {
        startY = Math.max(orginY3, lastTextObject.start.y)
      } else {
        if (orginY3 > 0) {
          startY = Math.max(orginY3, lastTextObject.start.y)
        } else {
          startY = Math.min(orginY3, lastTextObject.start.y)
        }
      }
    }

    if (orginX3 < 0) {
      orginX3 -= textWidth
    }

    var textObject = {
      lineStart: {
        x: orginX2,
        y: orginY2
      },
      lineEnd: {
        x: orginX1,
        y: orginY1
      },
      start: {
        x: orginX3,
        y: startY
      },
      width: textWidth,
      height: config.fontSize,
      text: item.text,
      color: item.color
    }

    lastTextObject = avoidCollision(textObject, lastTextObject)
    textObjectCollection.push(lastTextObject)
  })

  textObjectCollection.forEach(function (item) {
    var lineStartPoistion = convertCoordinateOrigin(item.lineStart.x, item.lineStart.y, center)
    var lineEndPoistion = convertCoordinateOrigin(item.lineEnd.x, item.lineEnd.y, center)
    var textPosition = convertCoordinateOrigin(item.start.x, item.start.y, center)
    context.setLineWidth(1)
    context.setFontSize(config.fontSize)
    context.beginPath()
    context.setStrokeStyle(item.color)
    context.setFillStyle(item.color)
    context.moveTo(lineStartPoistion.x, lineStartPoistion.y)
    var curveStartX = item.start.x < 0 ? textPosition.x + item.width : textPosition.x
    var textStartX = item.start.x < 0 ? textPosition.x - 5 : textPosition.x + 5
    context.quadraticCurveTo(lineEndPoistion.x, lineEndPoistion.y, curveStartX, textPosition.y)
    context.moveTo(lineStartPoistion.x, lineStartPoistion.y)
    context.stroke()
    context.closePath()
    context.beginPath()
    context.moveTo(textPosition.x + item.width, textPosition.y)
    context.arc(curveStartX, textPosition.y, 2, 0, 2 * Math.PI)
    context.closePath()
    context.fill()
    context.beginPath()
    context.setFillStyle('#666666')
    if (textStartX < 0) {
      var textLimitWidth = curveStartX - 10
      var textArr = calcTextArr(item.text, textLimitWidth).split(/\n/)
      for (var i = 0; i < textArr.length; i++) {
        context.fillText(textArr[i], 5, textPosition.y + 3 + i * (config.fontSize + 2))
      }
    } else {
      context.fillText(item.text, textStartX, textPosition.y + 3)
    }
    context.closePath()
    context.stroke()

    context.closePath()
  })
}

function drawToolTipSplitLine (offsetX, opts, config, context) {
  var startY = config.padding
  var endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight
  context.beginPath()
  context.setStrokeStyle('#cccccc')
  context.setLineWidth(1)
  context.moveTo(offsetX, startY)
  context.lineTo(offsetX, endY)
  context.stroke()
  context.closePath()
}

function drawToolTip (textList, offset, opts, config, context) {
  var legendWidth = 4
  var legendMarginRight = 5
  var arrowWidth = 8
  var isOverRightBorder = false
  offset = assign({
    x: 0,
    y: 0
  }, offset)
  offset.y -= 8
  var textWidth = textList.map(function (item) {
    return measureText(item.text)
  })

  var toolTipWidth = legendWidth + legendMarginRight + 4 * config.toolTipPadding + Math.max.apply(null, textWidth)
  var toolTipHeight = 2 * config.toolTipPadding + textList.length * config.toolTipLineHeight

  // if beyond the right border
  if (offset.x - Math.abs(opts._scrollDistance_) + arrowWidth + toolTipWidth > opts.width) {
    isOverRightBorder = true
  }

  // draw background rect
  context.beginPath()
  context.setFillStyle(opts.tooltip.option.background || config.toolTipBackground)
  context.setGlobalAlpha(config.toolTipOpacity)
  if (isOverRightBorder) {
    context.moveTo(offset.x, offset.y + 10)
    context.lineTo(offset.x - arrowWidth, offset.y + 10 - 5)
    context.lineTo(offset.x - arrowWidth, offset.y + 10 + 5)
    context.moveTo(offset.x, offset.y + 10)
    context.fillRect(offset.x - toolTipWidth - arrowWidth, offset.y, toolTipWidth, toolTipHeight)
  } else {
    context.moveTo(offset.x, offset.y + 10)
    context.lineTo(offset.x + arrowWidth, offset.y + 10 - 5)
    context.lineTo(offset.x + arrowWidth, offset.y + 10 + 5)
    context.moveTo(offset.x, offset.y + 10)
    context.fillRect(offset.x + arrowWidth, offset.y, toolTipWidth, toolTipHeight)
  }

  context.closePath()
  context.fill()
  context.setGlobalAlpha(1)

  // draw legend
  textList.forEach(function (item, index) {
    context.beginPath()
    context.setFillStyle(item.color)
    var startX = offset.x + arrowWidth + 2 * config.toolTipPadding
    var startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding
    if (isOverRightBorder) {
      startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding
    }
    context.fillRect(startX, startY, legendWidth, config.fontSize)
    context.closePath()
  })

  // draw text list
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle('#ffffff')
  textList.forEach(function (item, index) {
    var startX = offset.x + arrowWidth + 2 * config.toolTipPadding + legendWidth + legendMarginRight
    if (isOverRightBorder) {
      startX = offset.x - toolTipWidth - arrowWidth + 2 * config.toolTipPadding + +legendWidth + legendMarginRight
    }
    var startY = offset.y + (config.toolTipLineHeight - config.fontSize) / 2 + config.toolTipLineHeight * index + config.toolTipPadding
    context.fillText(item.text, startX, startY + config.fontSize)
  })
  context.stroke()
  context.closePath()
}

function drawYAxisTitle (title, opts, config, context) {
  var startX = config.xAxisHeight + (opts.height - config.xAxisHeight - measureText(title)) / 2
  context.save()
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle(opts.yAxis.titleFontColor || '#333333')
  context.translate(0, opts.height)
  context.rotate(-90 * Math.PI / 180)
  context.fillText(title, startX, config.padding + 0.5 * config.fontSize)
  context.stroke()
  context.closePath()
  context.restore()
}

function drawColumnDataPoints (series, opts, config, context) {
  var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  var _calYAxisData = calYAxisData(series, opts, config),
    ranges = _calYAxisData.ranges

  var _getXAxisPoints = getXAxisPoints(opts.categories, opts, config),
    xAxisPoints = _getXAxisPoints.xAxisPoints,
    eachSpacing = _getXAxisPoints.eachSpacing

  var minRange = ranges.pop()
  var maxRange = ranges.shift()
  context.save()
  if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
    context.translate(opts._scrollDistance_, 0)
  }

  series.forEach(function (eachSeries, seriesIndex) {
    var data = eachSeries.data
    var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process)
    points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts)

    // 绘制柱状数据图
    context.beginPath()
    context.setFillStyle(eachSeries.color)
    points.forEach(function (item, index) {
      if (item !== null) {
        var startX = item.x - item.width / 2 + 1
        var height = opts.height - item.y - config.padding - config.xAxisHeight - config.legendHeight
        context.moveTo(startX, item.y)
        context.rect(startX, item.y, item.width - 2, height)
      }
    })
    context.closePath()
    context.fill()
  })
  series.forEach(function (eachSeries, seriesIndex) {
    var data = eachSeries.data
    var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process)
    points = fixColumeData(points, eachSpacing, series.length, seriesIndex, config, opts)
    if (opts.dataLabel !== false && process === 1) {
      drawPointText(points, eachSeries, config, context)
    }
  })
  context.restore()
  return {
    xAxisPoints: xAxisPoints,
    eachSpacing: eachSpacing
  }
}

function drawAreaDataPoints (series, opts, config, context) {
  var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  var _calYAxisData2 = calYAxisData(series, opts, config),
    ranges = _calYAxisData2.ranges

  var _getXAxisPoints2 = getXAxisPoints(opts.categories, opts, config),
    xAxisPoints = _getXAxisPoints2.xAxisPoints,
    eachSpacing = _getXAxisPoints2.eachSpacing

  var minRange = ranges.pop()
  var maxRange = ranges.shift()
  var endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight
  var calPoints = []

  context.save()
  if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
    context.translate(opts._scrollDistance_, 0)
  }

  if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) {
    drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context)
  }

  series.forEach(function (eachSeries, seriesIndex) {
    var data = eachSeries.data
    var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process)
    calPoints.push(points)

    var splitPointList = splitPoints(points)

    splitPointList.forEach(function (points) {
      // 绘制区域数据
      context.beginPath()
      context.setStrokeStyle(eachSeries.color)
      context.setFillStyle(eachSeries.color)
      context.setGlobalAlpha(0.6)
      context.setLineWidth(2)
      if (points.length > 1) {
        var firstPoint = points[0]
        var lastPoint = points[points.length - 1]

        context.moveTo(firstPoint.x, firstPoint.y)
        if (opts.extra.lineStyle === 'curve') {
          points.forEach(function (item, index) {
            if (index > 0) {
              var ctrlPoint = createCurveControlPoints(points, index - 1)
              context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y)
            }
          })
        } else {
          points.forEach(function (item, index) {
            if (index > 0) {
              context.lineTo(item.x, item.y)
            }
          })
        }

        context.lineTo(lastPoint.x, endY)
        context.lineTo(firstPoint.x, endY)
        context.lineTo(firstPoint.x, firstPoint.y)
      } else {
        var item = points[0]
        context.moveTo(item.x - eachSpacing / 2, item.y)
        context.lineTo(item.x + eachSpacing / 2, item.y)
        context.lineTo(item.x + eachSpacing / 2, endY)
        context.lineTo(item.x - eachSpacing / 2, endY)
        context.moveTo(item.x - eachSpacing / 2, item.y)
      }
      context.closePath()
      context.fill()
      context.setGlobalAlpha(1)
    })

    if (opts.dataPointShape !== false) {
      var shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]
      drawPointShape(points, eachSeries.color, shape, context)
    }
  })
  if (opts.dataLabel !== false && process === 1) {
    series.forEach(function (eachSeries, seriesIndex) {
      var data = eachSeries.data
      var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process)
      drawPointText(points, eachSeries, config, context)
    })
  }

  context.restore()

  return {
    xAxisPoints: xAxisPoints,
    calPoints: calPoints,
    eachSpacing: eachSpacing
  }
}

function drawLineDataPoints (series, opts, config, context) {
  var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  var _calYAxisData3 = calYAxisData(series, opts, config),
    ranges = _calYAxisData3.ranges

  var _getXAxisPoints3 = getXAxisPoints(opts.categories, opts, config),
    xAxisPoints = _getXAxisPoints3.xAxisPoints,
    eachSpacing = _getXAxisPoints3.eachSpacing

  var minRange = ranges.pop()
  var maxRange = ranges.shift()
  var calPoints = []

  context.save()
  if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
    context.translate(opts._scrollDistance_, 0)
  }

  if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) {
    drawToolTipSplitLine(opts.tooltip.offset.x, opts, config, context)
  }

  series.forEach(function (eachSeries, seriesIndex) {
    var data = eachSeries.data
    var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process)
    calPoints.push(points)
    var splitPointList = splitPoints(points)

    splitPointList.forEach(function (points, index) {
      context.beginPath()
      context.setStrokeStyle(eachSeries.color)
      context.setLineWidth(2)
      if (points.length === 1) {
        context.moveTo(points[0].x, points[0].y)
        context.arc(points[0].x, points[0].y, 1, 0, 2 * Math.PI)
      } else {
        context.moveTo(points[0].x, points[0].y)
        if (opts.extra.lineStyle === 'curve') {
          points.forEach(function (item, index) {
            if (index > 0) {
              var ctrlPoint = createCurveControlPoints(points, index - 1)
              context.bezierCurveTo(ctrlPoint.ctrA.x, ctrlPoint.ctrA.y, ctrlPoint.ctrB.x, ctrlPoint.ctrB.y, item.x, item.y)
            }
          })
        } else {
          points.forEach(function (item, index) {
            if (index > 0) {
              context.lineTo(item.x, item.y)
            }
          })
        }
        context.moveTo(points[0].x, points[0].y)
      }
      context.closePath()
      context.stroke()
    })

    if (opts.dataPointShape !== false) {
      var shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]
      drawPointShape(points, eachSeries.color, shape, context)
    }
  })
  if (opts.dataLabel !== false && process === 1) {
    series.forEach(function (eachSeries, seriesIndex) {
      var data = eachSeries.data
      var points = getDataPoints(data, minRange, maxRange, xAxisPoints, eachSpacing, opts, config, process)
      drawPointText(points, eachSeries, config, context)
    })
  }

  context.restore()

  return {
    xAxisPoints: xAxisPoints,
    calPoints: calPoints,
    eachSpacing: eachSpacing
  }
}

function drawToolTipBridge (opts, config, context, process) {
  context.save()
  if (opts._scrollDistance_ && opts._scrollDistance_ !== 0 && opts.enableScroll === true) {
    context.translate(opts._scrollDistance_, 0)
  }
  if (opts.tooltip && opts.tooltip.textList && opts.tooltip.textList.length && process === 1) {
    drawToolTip(opts.tooltip.textList, opts.tooltip.offset, opts, config, context)
  }
  context.restore()
}

function drawXAxis (categories, opts, config, context) {
  var _getXAxisPoints4 = getXAxisPoints(categories, opts, config),
    xAxisPoints = _getXAxisPoints4.xAxisPoints,
    startX = _getXAxisPoints4.startX,
    endX = _getXAxisPoints4.endX,
    eachSpacing = _getXAxisPoints4.eachSpacing

  var startY = opts.height - config.padding - config.xAxisHeight - config.legendHeight
  var endY = startY + config.xAxisLineHeight

  context.save()
  if (opts._scrollDistance_ && opts._scrollDistance_ !== 0) {
    context.translate(opts._scrollDistance_, 0)
  }

  context.beginPath()
  context.setStrokeStyle(opts.xAxis.gridColor || '#cccccc')

  if (opts.xAxis.disableGrid !== true) {
    if (opts.xAxis.type === 'calibration') {
      xAxisPoints.forEach(function (item, index) {
        if (index > 0) {
          context.moveTo(item - eachSpacing / 2, startY)
          context.lineTo(item - eachSpacing / 2, startY + 4)
        }
      })
    } else {
      xAxisPoints.forEach(function (item, index) {
        context.moveTo(item, startY)
        context.lineTo(item, endY)
      })
    }
  }
  context.closePath()
  context.stroke()

  // 对X轴列表做抽稀处理
  var validWidth = opts.width - 2 * config.padding - config.yAxisWidth - config.yAxisTitleWidth
  var maxXAxisListLength = Math.min(categories.length, Math.ceil(validWidth / config.fontSize / 1.5))
  var ratio = Math.ceil(categories.length / maxXAxisListLength)

  categories = categories.map(function (item, index) {
    return index % ratio !== 0 ? '' : item
  })

  if (config._xAxisTextAngle_ === 0) {
    context.beginPath()
    context.setFontSize(config.fontSize)
    context.setFillStyle(opts.xAxis.fontColor || '#666666')
    categories.forEach(function (item, index) {
      var offset = eachSpacing / 2 - measureText(item) / 2
      context.fillText(item, xAxisPoints[index] + offset, startY + config.fontSize + 5)
    })
    context.closePath()
    context.stroke()
  } else {
    categories.forEach(function (item, index) {
      context.save()
      context.beginPath()
      context.setFontSize(config.fontSize)
      context.setFillStyle(opts.xAxis.fontColor || '#666666')
      var textWidth = measureText(item)
      var offset = eachSpacing / 2 - textWidth

      var _calRotateTranslate = calRotateTranslate(xAxisPoints[index] + eachSpacing / 2, startY + config.fontSize / 2 + 5, opts.height),
        transX = _calRotateTranslate.transX,
        transY = _calRotateTranslate.transY

      context.rotate(-1 * config._xAxisTextAngle_)
      context.translate(transX, transY)
      context.fillText(item, xAxisPoints[index] + offset, startY + config.fontSize + 5)
      context.closePath()
      context.stroke()
      context.restore()
    })
  }

  context.restore()
}

function drawYAxisGrid (opts, config, context) {
  var spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
  var eachSpacing = Math.floor(spacingValid / config.yAxisSplit)
  var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth
  var startX = config.padding + yAxisTotalWidth
  var endX = opts.width - config.padding

  var points = []
  for (var i = 0; i < config.yAxisSplit; i++) {
    points.push(config.padding + eachSpacing * i)
  }
  points.push(config.padding + eachSpacing * config.yAxisSplit + 2)

  context.beginPath()
  context.setStrokeStyle(opts.yAxis.gridColor || '#cccccc')
  context.setLineWidth(1)
  points.forEach(function (item, index) {
    context.moveTo(startX, item)
    context.lineTo(endX, item)
  })
  context.closePath()
  context.stroke()
}

function drawYAxis (series, opts, config, context) {
  if (opts.yAxis.disabled === true) {
    return
  }

  var _calYAxisData4 = calYAxisData(series, opts, config),
    rangesFormat = _calYAxisData4.rangesFormat

  var yAxisTotalWidth = config.yAxisWidth + config.yAxisTitleWidth

  var spacingValid = opts.height - 2 * config.padding - config.xAxisHeight - config.legendHeight
  var eachSpacing = Math.floor(spacingValid / config.yAxisSplit)
  var startX = config.padding + yAxisTotalWidth
  var endX = opts.width - config.padding
  var endY = opts.height - config.padding - config.xAxisHeight - config.legendHeight

  // set YAxis background
  context.setFillStyle(opts.background || '#ffffff')
  if (opts._scrollDistance_ < 0) {
    context.fillRect(0, 0, startX, endY + config.xAxisHeight + 5)
  }
  context.fillRect(endX, 0, opts.width, endY + config.xAxisHeight + 5)

  var points = []
  for (var i = 0; i <= config.yAxisSplit; i++) {
    points.push(config.padding + eachSpacing * i)
  }

  context.stroke()
  context.beginPath()
  context.setFontSize(config.fontSize)
  context.setFillStyle(opts.yAxis.fontColor || '#666666')
  rangesFormat.forEach(function (item, index) {
    var pos = points[index] ? points[index] : endY
    context.fillText(item, config.padding + config.yAxisTitleWidth, pos + config.fontSize / 2)
  })
  context.closePath()
  context.stroke()

  if (opts.yAxis.title) {
    drawYAxisTitle(opts.yAxis.title, opts, config, context)
  }
}

function drawLegend (series, opts, config, context) {
  if (!opts.legend) {
    return
  }
  // each legend shape width 15px
  // the spacing between shape and text in each legend is the `padding`
  // each legend spacing is the `padding`
  // legend margin top `config.padding`

  var _calLegendData = calLegendData(series, opts, config),
    legendList = _calLegendData.legendList

  var padding = 5
  var marginTop = 8
  var shapeWidth = 15
  legendList.forEach(function (itemList, listIndex) {
    var width = 0
    itemList.forEach(function (item) {
      item.name = item.name || 'undefined'
      width += 3 * padding + measureText(item.name) + shapeWidth
    })
    var startX = (opts.width - width) / 2 + padding
    var startY = opts.height - config.padding - config.legendHeight + listIndex * (config.fontSize + marginTop) + padding + marginTop

    context.setFontSize(config.fontSize)
    itemList.forEach(function (item) {
      switch (opts.type) {
        case 'line':
          context.beginPath()
          context.setLineWidth(1)
          context.setStrokeStyle(item.color)
          context.moveTo(startX - 2, startY + 5)
          context.lineTo(startX + 17, startY + 5)
          context.stroke()
          context.closePath()
          context.beginPath()
          context.setLineWidth(1)
          context.setStrokeStyle('#ffffff')
          context.setFillStyle(item.color)
          context.moveTo(startX + 7.5, startY + 5)
          context.arc(startX + 7.5, startY + 5, 4, 0, 2 * Math.PI)
          context.fill()
          context.stroke()
          context.closePath()
          break
        case 'pie':
        case 'ring':
          context.beginPath()
          context.setFillStyle(item.color)
          context.moveTo(startX + 7.5, startY + 5)
          context.arc(startX + 7.5, startY + 5, 7, 0, 2 * Math.PI)
          context.closePath()
          context.fill()
          break
        default:
          context.beginPath()
          context.setFillStyle(item.color)
          context.moveTo(startX, startY)
          context.rect(startX, startY, 15, 10)
          context.closePath()
          context.fill()
      }
      startX += padding + shapeWidth
      context.beginPath()
      context.setFillStyle(opts.extra.legendTextColor || '#333333')
      context.fillText(item.name, startX, startY + 9)
      context.closePath()
      context.stroke()
      startX += measureText(item.name) + 2 * padding
    })
  })
}

function drawPieDataPoints (series, opts, config, context) {
  var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  var pieOption = opts.extra.pie || {}
  series = getPieDataPoints(series, process)
  var centerPosition = {
    x: opts.width / 2,
    y: (opts.height - config.legendHeight) / 2
  }
  var radius = Math.min(centerPosition.x - config.pieChartLinePadding - config.pieChartTextPadding/* - config._pieTextMaxLength_ */, centerPosition.y - config.pieChartLinePadding - config.pieChartTextPadding)
  if (opts.dataLabel) {
    radius -= 10
  } else {
    radius -= 2 * config.padding
  }
  series = series.map(function (eachSeries) {
    eachSeries._start_ += (pieOption.offsetAngle || 0) * Math.PI / 180
    return eachSeries
  })
  series.forEach(function (eachSeries) {
    context.beginPath()
    context.setLineWidth(2)
    context.setStrokeStyle('#ffffff')
    context.setFillStyle(eachSeries.color)
    context.moveTo(centerPosition.x, centerPosition.y)
    context.arc(centerPosition.x, centerPosition.y, radius, eachSeries._start_, eachSeries._start_ + 2 * eachSeries._proportion_ * Math.PI)
    context.closePath()
    context.fill()
    if (opts.disablePieStroke !== true) {
      context.stroke()
    }
  })

  if (opts.type === 'ring') {
    var innerPieWidth = radius * 0.6
    if (typeof opts.extra.ringWidth === 'number' && opts.extra.ringWidth > 0) {
      innerPieWidth = Math.max(0, radius - opts.extra.ringWidth)
    }
    context.beginPath()
    context.setFillStyle(opts.background || '#ffffff')
    context.moveTo(centerPosition.x, centerPosition.y)
    context.arc(centerPosition.x, centerPosition.y, innerPieWidth, 0, 2 * Math.PI)
    context.closePath()
    context.fill()
  }

  if (opts.dataLabel !== false && process === 1) {
    // fix https://github.com/xiaolin3303/wx-charts/issues/132
    var valid = false
    for (var i = 0, len = series.length; i < len; i++) {
      if (series[i].data > 0) {
        valid = true
        break
      }
    }

    if (valid) {
      drawPieText(series, opts, config, context, radius, centerPosition)
    }
  }

  if (process === 1 && opts.type === 'ring') {
    drawRingTitle(opts, config, context)
  }

  return {
    center: centerPosition,
    radius: radius,
    series: series
  }
}

function drawRadarDataPoints (series, opts, config, context) {
  var process = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 1

  var radarOption = opts.extra.radar || {}
  var coordinateAngle = getRadarCoordinateSeries(opts.categories.length)
  var centerPosition = {
    x: opts.width / 2,
    y: (opts.height - config.legendHeight) / 2
  }

  var radius = Math.min(centerPosition.x - (getMaxTextListLength(opts.categories) + config.radarLabelTextMargin), centerPosition.y - config.radarLabelTextMargin)

  radius -= config.padding

  // draw grid
  context.beginPath()
  context.setLineWidth(1)
  context.setStrokeStyle(radarOption.gridColor || '#cccccc')
  coordinateAngle.forEach(function (angle) {
    var pos = convertCoordinateOrigin(radius * Math.cos(angle), radius * Math.sin(angle), centerPosition)
    context.moveTo(centerPosition.x, centerPosition.y)
    context.lineTo(pos.x, pos.y)
  })
  context.stroke()
  context.closePath()

  // draw split line grid

  var _loop = function _loop (i) {
    var startPos = {}
    context.beginPath()
    context.setLineWidth(1)
    context.setStrokeStyle(radarOption.gridColor || '#cccccc')
    coordinateAngle.forEach(function (angle, index) {
      var pos = convertCoordinateOrigin(radius / config.radarGridCount * i * Math.cos(angle), radius / config.radarGridCount * i * Math.sin(angle), centerPosition)
      if (index === 0) {
        startPos = pos
        context.moveTo(pos.x, pos.y)
      } else {
        context.lineTo(pos.x, pos.y)
      }
    })
    context.lineTo(startPos.x, startPos.y)
    context.stroke()
    context.closePath()
  }

  for (var i = 1; i <= config.radarGridCount; i++) {
    _loop(i)
  }

  var radarDataPoints = getRadarDataPoints(coordinateAngle, centerPosition, radius, series, opts, process)
  radarDataPoints.forEach(function (eachSeries, seriesIndex) {
    // 绘制区域数据
    context.beginPath()
    context.setFillStyle(eachSeries.color)
    context.setGlobalAlpha(0.6)
    eachSeries.data.forEach(function (item, index) {
      if (index === 0) {
        context.moveTo(item.position.x, item.position.y)
      } else {
        context.lineTo(item.position.x, item.position.y)
      }
    })
    context.closePath()
    context.fill()
    context.setGlobalAlpha(1)

    if (opts.dataPointShape !== false) {
      var shape = config.dataPointShape[seriesIndex % config.dataPointShape.length]
      var points = eachSeries.data.map(function (item) {
        return item.position
      })
      drawPointShape(points, eachSeries.color, shape, context)
    }
  })
  // draw label text
  drawRadarLabel(coordinateAngle, radius, centerPosition, opts, config, context)

  return {
    center: centerPosition,
    radius: radius,
    angleList: coordinateAngle
  }
}

function drawCanvas (opts, context) {
  context.draw()
}

var Timing = {
  easeIn: function easeIn (pos) {
    return Math.pow(pos, 3)
  },

  easeOut: function easeOut (pos) {
    return Math.pow(pos - 1, 3) + 1
  },

  easeInOut: function easeInOut (pos) {
    if ((pos /= 0.5) < 1) {
      return 0.5 * Math.pow(pos, 3)
    } else {
      return 0.5 * (Math.pow(pos - 2, 3) + 2)
    }
  },

  linear: function linear (pos) {
    return pos
  }
}

function Animation (opts) {
  this.isStop = false
  opts.duration = typeof opts.duration === 'undefined' ? 1000 : opts.duration
  opts.timing = opts.timing || 'linear'

  var delay = 16.7

  var createAnimationFrame = function createAnimationFrame () {
    if (typeof requestAnimationFrame !== 'undefined') {
      return requestAnimationFrame
    } else if (typeof setTimeout !== 'undefined') {
      return function (step, delay) {
        setTimeout(function () {
          var timeStamp = +new Date()
          step(timeStamp)
        }, delay)
      }
    } else {
      return function (step) {
        step(null)
      }
    }
  }
  var animationFrame = createAnimationFrame()
  var startTimeStamp = null
  var _step = function step (timestamp) {
    if (timestamp === null || this.isStop === true) {
      opts.onProcess && opts.onProcess(1)
      opts.onAnimationFinish && opts.onAnimationFinish()
      return
    }
    if (startTimeStamp === null) {
      startTimeStamp = timestamp
    }
    if (timestamp - startTimeStamp < opts.duration) {
      var process = (timestamp - startTimeStamp) / opts.duration
      var timingFunction = Timing[opts.timing]
      process = timingFunction(process)
      opts.onProcess && opts.onProcess(process)
      animationFrame(_step, delay)
    } else {
      opts.onProcess && opts.onProcess(1)
      opts.onAnimationFinish && opts.onAnimationFinish()
    }
  }
  _step = _step.bind(this)

  animationFrame(_step, delay)
}

// stop animation immediately
// and tigger onAnimationFinish
Animation.prototype.stop = function () {
  this.isStop = true
}

function drawCharts (type, opts, config, context) {
  var _this = this

  var series = opts.series
  var categories = opts.categories
  series = fillSeriesColor(series, config)

  var _calLegendData = calLegendData(series, opts, config),
    legendHeight = _calLegendData.legendHeight

  config.legendHeight = legendHeight

  var _calYAxisData = calYAxisData(series, opts, config),
    yAxisWidth = _calYAxisData.yAxisWidth

  config.yAxisWidth = yAxisWidth
  if (categories && categories.length) {
    var _calCategoriesData = calCategoriesData(categories, opts, config),
      xAxisHeight = _calCategoriesData.xAxisHeight,
      angle = _calCategoriesData.angle

    config.xAxisHeight = xAxisHeight
    config._xAxisTextAngle_ = angle
  }
  // if (type === 'pie' || type === 'ring') {
  //   config._pieTextMaxLength_ = opts.dataLabel === false ? 0 : (opts.ignoreLabelWidth ? 0 : getPieTextMaxLength(series))
  // }

  var duration = opts.animation ? 1000 : 0
  this.animationInstance && this.animationInstance.stop()
  switch (type) {
    case 'line':
      this.animationInstance = new Animation({
        timing: 'easeIn',
        duration: duration,
        onProcess: function onProcess (process) {
          drawYAxisGrid(opts, config, context)

          var _drawLineDataPoints = drawLineDataPoints(series, opts, config, context, process),
            xAxisPoints = _drawLineDataPoints.xAxisPoints,
            calPoints = _drawLineDataPoints.calPoints,
            eachSpacing = _drawLineDataPoints.eachSpacing

          _this.chartData.xAxisPoints = xAxisPoints
          _this.chartData.calPoints = calPoints
          _this.chartData.eachSpacing = eachSpacing
          drawXAxis(categories, opts, config, context)
          drawLegend(opts.series, opts, config, context)
          drawYAxis(series, opts, config, context)
          drawToolTipBridge(opts, config, context, process)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish () {
          _this.event.trigger('renderComplete')
        }
      })
      break
    case 'column':
      this.animationInstance = new Animation({
        timing: 'easeIn',
        duration: duration,
        onProcess: function onProcess (process) {
          drawYAxisGrid(opts, config, context)

          var _drawColumnDataPoints = drawColumnDataPoints(series, opts, config, context, process),
            xAxisPoints = _drawColumnDataPoints.xAxisPoints,
            eachSpacing = _drawColumnDataPoints.eachSpacing

          _this.chartData.xAxisPoints = xAxisPoints
          _this.chartData.eachSpacing = eachSpacing
          drawXAxis(categories, opts, config, context)
          drawLegend(opts.series, opts, config, context)
          drawYAxis(series, opts, config, context)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish () {
          _this.event.trigger('renderComplete')
        }
      })
      break
    case 'area':
      this.animationInstance = new Animation({
        timing: 'easeIn',
        duration: duration,
        onProcess: function onProcess (process) {
          drawYAxisGrid(opts, config, context)

          var _drawAreaDataPoints = drawAreaDataPoints(series, opts, config, context, process),
            xAxisPoints = _drawAreaDataPoints.xAxisPoints,
            calPoints = _drawAreaDataPoints.calPoints,
            eachSpacing = _drawAreaDataPoints.eachSpacing

          _this.chartData.xAxisPoints = xAxisPoints
          _this.chartData.calPoints = calPoints
          _this.chartData.eachSpacing = eachSpacing
          drawXAxis(categories, opts, config, context)
          drawLegend(opts.series, opts, config, context)
          drawYAxis(series, opts, config, context)
          drawToolTipBridge(opts, config, context, process)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish () {
          _this.event.trigger('renderComplete')
        }
      })
      break
    case 'ring':
    case 'pie':
      this.animationInstance = new Animation({
        timing: 'easeInOut',
        duration: duration,
        onProcess: function onProcess (process) {
          _this.chartData.pieData = drawPieDataPoints(series, opts, config, context, process)
          drawLegend(opts.series, opts, config, context)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish () {
          _this.event.trigger('renderComplete')
        }
      })
      break
    case 'radar':
      this.animationInstance = new Animation({
        timing: 'easeInOut',
        duration: duration,
        onProcess: function onProcess (process) {
          _this.chartData.radarData = drawRadarDataPoints(series, opts, config, context, process)
          drawLegend(opts.series, opts, config, context)
          drawCanvas(opts, context)
        },
        onAnimationFinish: function onAnimationFinish () {
          _this.event.trigger('renderComplete')
        }
      })
      break
  }
}

// simple event implement

function Event () {
  this.events = {}
}

Event.prototype.addEventListener = function (type, listener) {
  this.events[type] = this.events[type] || []
  this.events[type].push(listener)
}

Event.prototype.trigger = function () {
  for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
    args[_key] = arguments[_key]
  }

  var type = args[0]
  var params = args.slice(1)
  if (!!this.events[type]) {
    this.events[type].forEach(function (listener) {
      try {
        listener.apply(null, params)
      } catch (e) {
        console.error(e)
      }
    })
  }
}

var Charts = function Charts (opts) {
  opts.title = opts.title || {}
  opts.subtitle = opts.subtitle || {}
  opts.yAxis = opts.yAxis || {}
  opts.xAxis = opts.xAxis || {}
  opts.extra = opts.extra || {}
  opts.legend = opts.legend === false ? false : true
  opts.animation = opts.animation === false ? false : true
  var config$$1 = assign({}, config)
  config$$1.yAxisTitleWidth = opts.yAxis.disabled !== true && opts.yAxis.title ? config$$1.yAxisTitleWidth : 0
  config$$1.pieChartLinePadding = opts.dataLabel === false ? 0 : config$$1.pieChartLinePadding
  config$$1.pieChartTextPadding = opts.dataLabel === false ? 0 : config$$1.pieChartTextPadding

  this.opts = opts
  this.config = config$$1
  this.context = wx.createCanvasContext(opts.canvasId)
  // store calcuated chart data
  // such as chart point coordinate
  this.chartData = {}
  this.event = new Event()
  this.scrollOption = {
    currentOffset: 0,
    startTouchX: 0,
    distance: 0
  }

  drawCharts.call(this, opts.type, opts, config$$1, this.context)
}

Charts.prototype.updateData = function () {
  var data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}

  this.opts.series = data.series || this.opts.series
  this.opts.categories = data.categories || this.opts.categories

  this.opts.title = assign({}, this.opts.title, data.title || {})
  this.opts.subtitle = assign({}, this.opts.subtitle, data.subtitle || {})

  drawCharts.call(this, this.opts.type, this.opts, this.config, this.context)
}

Charts.prototype.stopAnimation = function () {
  this.animationInstance && this.animationInstance.stop()
}

Charts.prototype.addEventListener = function (type, listener) {
  this.event.addEventListener(type, listener)
}

Charts.prototype.getCurrentDataIndex = function (e) {
  var touches = e.touches && e.touches.length ? e.touches : e.changedTouches
  if (touches && touches.length) {
    var _touches$ = touches[0],
      x = _touches$.x,
      y = _touches$.y

    if (this.opts.type === 'pie' || this.opts.type === 'ring') {
      return findPieChartCurrentIndex({x: x, y: y}, this.chartData.pieData)
    } else if (this.opts.type === 'radar') {
      return findRadarChartCurrentIndex({x: x, y: y}, this.chartData.radarData, this.opts.categories.length)
    } else {
      return findCurrentIndex({
        x: x,
        y: y
      }, this.chartData.xAxisPoints, this.opts, this.config, Math.abs(this.scrollOption.currentOffset))
    }
  }
  return -1
}

Charts.prototype.showToolTip = function (e) {
  var option = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}

  if (this.opts.type === 'line' || this.opts.type === 'area') {
    var index = this.getCurrentDataIndex(e)
    var currentOffset = this.scrollOption.currentOffset

    var opts = assign({}, this.opts, {
      _scrollDistance_: currentOffset,
      animation: false
    })
    if (index > -1) {
      var seriesData = getSeriesDataItem(this.opts.series, index)
      if (seriesData.length === 0) {
        drawCharts.call(this, opts.type, opts, this.config, this.context)
      } else {
        var _getToolTipData = getToolTipData(seriesData, this.chartData.calPoints, index, this.opts.categories, option),
          textList = _getToolTipData.textList,
          offset = _getToolTipData.offset

        opts.tooltip = {
          textList: textList,
          offset: offset,
          option: option
        }
        drawCharts.call(this, opts.type, opts, this.config, this.context)
      }
    } else {
      drawCharts.call(this, opts.type, opts, this.config, this.context)
    }
  }
}

Charts.prototype.scrollStart = function (e) {
  if (e.touches[0] && this.opts.enableScroll === true) {
    this.scrollOption.startTouchX = e.touches[0].x
  }
}

Charts.prototype.scroll = function (e) {
  // TODO throtting...
  if (e.touches[0] && this.opts.enableScroll === true) {
    var _distance = e.touches[0].x - this.scrollOption.startTouchX
    var currentOffset = this.scrollOption.currentOffset

    var validDistance = calValidDistance(currentOffset + _distance, this.chartData, this.config, this.opts)

    this.scrollOption.distance = _distance = validDistance - currentOffset
    var opts = assign({}, this.opts, {
      _scrollDistance_: currentOffset + _distance,
      animation: false
    })

    drawCharts.call(this, opts.type, opts, this.config, this.context)
  }
}

Charts.prototype.scrollEnd = function (e) {
  if (this.opts.enableScroll === true) {
    var _scrollOption = this.scrollOption,
      currentOffset = _scrollOption.currentOffset,
      distance = _scrollOption.distance

    this.scrollOption.currentOffset = currentOffset + distance
    this.scrollOption.distance = 0
  }
}

module.exports = Charts
